<?php
/*
 * MiniMe - a minimalistic OStatus complient microblogging tool
 * Copyright (C) 2010, Tuomas Koski - koski.tuomas@gmail.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * This class is representing a XML feed similar to this:
 * <feed xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 *  <generator uri="http://madebymonsieur.com" version="0.0">MiniMe</generator>
 *  <id><? echo 'http://lobstermonster.org/minime/' . time() ;?></id>
 *  <title>My timeline!</title>
 *  <subtitle>My timeline provided by minime mini system. Minimalic!</subtitle>
 *  <logo></logo>
 *  <updated>2010-07-21T23:25:59+00:00</updated>
 * <author>
 *  <name>Tuomas Koski</name>
 *  <uri>http://lobstermonster.org/minime/</uri>
 * </author>
 *  <link href="http://lobstermonster.org/minime/" rel="alternate" type="text/html"/>
 *  <link href="http://lobstermonster.org/minime/hub.php" rel="hub"/>
 *  <link href="http://lobstermonster.org/minime/salmon-replies.php" rel="http://salmon-protocol.org/ns/salmon-replies"/>
 *  <link href="http://lobstermonster.org/minime/salmon-mention.php" rel="http://salmon-protocol.org/ns/salmon-mention"/>
 *  <link href="http://lobstermonster.org/minime/atom.php" rel="self" type="application/atom+xml"/>
 * <activity:subject>
 *  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 *  <id>http://lobstermonster.org/minime</id>
 *  <title>Tuomas Koski</title>
 *  <link rel="alternate" type="text/html" href="http://identi.ca/tkoski"/>
 * </activity:subject>
 * </feed>
 * 
 * @author     Tuomas Koski <koski.tuomas@gmail.com>
 * @copyright  Tuomas Koski
 * @license    http://www.gnu.org/licenses/lgpl.txt
 */
class Atom_Feed {
    
    const ELEMENT_FEED = "feed";
    
    const SORT_ENTRIES_BY_UPDATED_TIMESTAMP = TRUE;
    const DO_NOT_SORT_ENTRIES_BY_UPDATED_TIMESTAMP = FALSE;
    
    private $namespaces = array();
    
    /**
     * @var Array of Atom_ChildElement
     */
    private $childElements = array();
    
    private $domElements = array();
    
    public function __construct() {
    }
    
    public function addEntry(Atom_Entry $entry) {
        if($entry == null) {
            return false;
        }
        
        $entries = $this->getEntries(self::DO_NOT_SORT_ENTRIES_BY_UPDATED_TIMESTAMP);
        
        if(isset($entries[$entry->getId()->getId()])) {
            // Entry with this ID already exists.
            return false;
        }
        
        return $this->addChildElement($entry);
    }
    
    public function addChildElement(Atom_ChildElement $childElement) {
        if($childElement == null) {
            return false;
        }
        $this->childElements[] = $childElement;
        return true;
    }
    
    /**
     * @return Atom_Feed_Link_Hub
     */
    public function getHub() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Link_Hub) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Atom_Feed_Link_Hub
     */
    public function getSalmon() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Link_Salmon) {
                return $elm;
            }
        }
        return null;
    }
    
    public function updateUpdated() {
        foreach ($this->childElements as $k => $elm) {
            if($elm instanceof Atom_Feed_Updated) {
                unset($this->childElements[$k]);
                break;
            }
        }
        $this->addChildElement(new Atom_Feed_Updated());
    }
    
    /**
     * @return Atom_Feed_Link_Logo
     */
    public function getLogo() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Logo) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Atom_Feed_Author
     */
    public function getAuthor() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Author) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Atom_Feed_Link_AlternateHtml
     */
    public function getAlternateHtml() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Link_AlternateHtml) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Atom_Feed_Title
     */
    public function getTitle() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Feed_Title) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Activitystream_Subject
     */
    public function getActivityStreamSubject() {
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Activitystream_Subject) {
                return $elm;
            }
        }
        return null;
    }
    
    /**
     * @return Array 
     */
    public function getEntries($sortByTime = self::SORT_ENTRIES_BY_UPDATED_TIMESTAMP) {
        
        $ret = array();
        reset($this->childElements);
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Entry) {
                $ret[$elm->getId()->getId()] = $elm;
            }
        }
        if($sortByTime) {
            uasort($ret, array("Atom_Feed", "cmp_entries"));
        }
        return $ret;
    }
    
    /**
     * @return Array 
     */
    public function getEntriesGrouped() {
        
        $entriesRAW = $this->getEntries(self::DO_NOT_SORT_ENTRIES_BY_UPDATED_TIMESTAMP);
        $entriesTMP = array();
        foreach ($entriesRAW as $entry) {
            $entriesTMP[$entry->getId()->getId()]['entry'] = $entry;
        }
        
        $entries = array();
        foreach ($entriesTMP as $entry) {
            if($entry->getInReplyTo()== null) {
                continue;
            }
            $entries[$entry->getInReplyTo()->getRef()]['replies'][$entry->getId()->getId()] = $entry;
        }
    }
    
    public function removeEntry($id) {
        reset($this->childElements);
        foreach ($this->childElements as $k => $elm) {
            if(!$elm instanceof Atom_Entry) {
                continue;
            }
            
            if($elm->getId()->getId() == $id) {
                unset($this->childElements[$k]);
                break;
            }
        }
        reset($this->childElements);
    }
    
    public function addDOMElement(DOMElement $elm) {
        $this->domElements[] = $elm;
    }
    
    public function addNamespace($namespaceURI, $qualifiedName) {
        $this->namespaces['xmlns:' . $namespaceURI] = $qualifiedName;
    }
    
    private function getNamespaces() {
        return $this->namespaces;
    }
    
    public function setNamespaces(Array $namespaces) {
        $this->namespaces = $namespaces;
    }
    
    public function asXML() {
        
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->formatOutput = true;
        
        $entry_element = $dom->createElementNS(Atom::ATOM_NS, self::ELEMENT_FEED);

        reset($this->namespaces);
        foreach ($this->namespaces as $namespaceURI => $qualifiedName) {
            $entry_element->setAttributeNS('http://www.w3.org/2000/xmlns/', $namespaceURI, $qualifiedName);
        }
        
        reset($this->childElements);
        foreach ($this->childElements as $childElement) {
            $entry_element->appendChild($childElement->asDomElement($dom));
        } 
        
        reset($this->domElements);
        foreach ($this->domElements as $elm) {
            $newEntry = $dom->importNode($elm, true);
            $entry_element->appendChild($newEntry);
        } 
        
        $dom->appendChild($entry_element);
        
        return $dom->saveXML();	    
    }
    
    public function __toString() {
        return $this->asXML();
    }
    
    /**
     * Terrible method. fix this.
     * @param unknown_type $elm
     * @param unknown_type $childElementName
     * @return unknown_type
     */
    public static function getFirstElementByTagName($elm, $childElementName) {
        $elements = $elm->getElementsByTagName($childElementName);
        if($elements->length === 0) {
            return null;
        }
        return $elements->item(0);
    }
    
    public function getFirstElementWithXPathQuery($query, $doc) {
        $xpath = new DOMXPath($doc);
        $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
        
        $elements = $xpath->query($query);
        if($elements->length === 0) {
            return null;
        }
        
        return $elements->item(0);
    }
    
    /**
     * 
     * @param String $rawXml
     * @return Atom_Feed
     */
    public static function buildFromRawXML($rawXml) {
        $dom = new DOMDocument();
        if(@$dom->loadXML($rawXml)) {
            return self::buildFromDom($dom);
        }
        throw new Exception("Invalid raw XML data provided!");
    }
    
    /**
     * This logic is terrible. Fix this.
     * @param DOMDocument $dom
     * @return Atom_Feed
     */
    public static function buildFromDom(DOMDocument $dom) {
        
        $feed_element = $dom->getElementsByTagName(self::ELEMENT_FEED)->item(0);
        if (!$feed_element) {
            throw new Exception("Invalid DOM object for Atom_Feed, missing feed root");
        }
        
        $atom = new Atom_Feed();
        
        // Generator is optional
        // $generator = self::getFirstElementByTagName($feed_element, Atom_Feed_Generator::ELEMENT_GENERATOR);
        $generator = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:generator', $dom);
        if($generator !== null) {
            $generator = Atom_Feed_Generator::buildFromDOMElement($generator);
            $atom->addChildElement($generator);
        }
        
        //$id = Atom_Feed_Id::buildFromDOMElement(self::getFirstElementByTagName($feed_element, Atom_Feed_Id::ELEMENT_ID));
        $id = Atom_Feed_Id::buildFromDOMElement($atom->getFirstElementWithXPathQuery('//atom:feed/atom:id', $dom));
        $atom->addChildElement($id);
        
        //$title = Atom_Feed_Title::buildFromDOMElement(self::getFirstElementByTagName($feed_element, Atom_Feed_Title::ELEMENT_TITLE));
        $title = Atom_Feed_Title::buildFromDOMElement($atom->getFirstElementWithXPathQuery('//atom:feed/atom:title', $dom));
        $atom->addChildElement($title);
        
        // Subtitle is optional
        //$subtitle = self::getFirstElementByTagName($feed_element, Atom_Feed_Subtitle::ELEMENT_SUBTITLE);
        $subtitle = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:subtitle', $dom);
        if($subtitle != null) {
            $subtitle = Atom_Feed_Subtitle::buildFromDOMElement($subtitle);
            $atom->addChildElement($subtitle);
        }
        // Logo is optional
        //$logo  = self::getFirstElementByTagName($feed_element, Atom_Feed_Logo::ELEMENT_LOGO);
        $logo = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:logo', $dom);
        if($logo) {
            $logo = Atom_Feed_Logo::buildFromDOMElement($logo);
            $atom->addChildElement($logo);
        }

        $updated = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:updated[position()=1]', $dom);
        if($updated === null) {
           $updated = new Atom_Feed_Updated(); 
        } else {
            $updated = Atom_Feed_Updated::buildFromDOMElement($updated);
        }
        $atom->addChildElement($updated);
        
        //$author = Atom_Feed_Author::buildFromDOMElement(self::getFirstElementByTagName($feed_element, Atom_Feed_Author::ELEMENT_AUTHOR));
        $author = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:author', $dom);
        if($author === null) {
            //This hack is for cliqset.
            $author = $atom->getFirstElementWithXPathQuery('//atom:feed/atom:entry/atom:author[1]', $dom);
        }
        $author = Atom_Feed_Author::buildFromDOMElement($author);
        $atom->addChildElement($author);
        
        foreach ($feed_element->getElementsByTagName(Atom_Feed_Link::ELEMENT_LINK) as $elmLink) {
            
            if( $elmLink->parentNode->nodeName !== self::ELEMENT_FEED) {
                continue;
            }
            
            if($elmLink->getAttribute(Atom_Feed_Link_AlternateHtml::ATTRIBUTE_REL) == Atom_Feed_Link_AlternateHtml::REL_VALUE) {
                $link = Atom_Feed_Link_AlternateHtml::buildFromDOMElement($elmLink); 
                $atom->addChildElement($link);
            } else if($elmLink->getAttribute(Atom_Feed_Link_Hub::ATTRIBUTE_REL) == Atom_Feed_Link_Hub::REL_VALUE) {
                $link = Atom_Feed_Link_Hub::buildFromDOMElement($elmLink);  
                $atom->addChildElement($link);    
            } else if($elmLink->getAttribute(Atom_Feed_Link_Salmon::ATTRIBUTE_REL) == Atom_Feed_Link_Salmon::REL_VALUE) {
                $link = Atom_Feed_Link_Salmon::buildFromDOMElement($elmLink);
                $atom->addChildElement($link);
            } else if($elmLink->getAttribute(Atom_Feed_Link_Self::ATTRIBUTE_REL) == Atom_Feed_Link_Self::REL_VALUE) {
                $link = Atom_Feed_Link_Self::buildFromDOMElement($elmLink);
                $atom->addChildElement($link);
            } else if($elmLink->getAttribute(Atom_Feed_Link_Avatar::ATTRIBUTE_REL) == Atom_Feed_Link_Avatar::REL_VALUE) {
                $link = Atom_Feed_Link_Avatar::buildFromDOMElement($elmLink);
                $atom->addChildElement($link);
            } 
        }
        
        // another elgg bug
        if ($atom->getAlternateHtml() === null) {
            $link = new Atom_Feed_Link_AlternateHtml($id->getId()); 
            $atom->addChildElement($link);
        }
        
        $ac = $feed_element->getElementsByTagNameNS(ActivityStream::AS_NS, Activitystream_Subject::ELEMENT_SUBJECT);
        if($ac->length > 0) {
            $atom->addChildElement(Activitystream_Subject::buildFromDOMElement($ac->item(0)));
        }
        
        $atom->addNamespace(ActivityStream::AS_NS_PREFIX, Activitystream::AS_NS);
        $atom->addNamespace(Thr::NS_PREFIX, Thr::NS);
                                                                     
        // FINISH THIS! Add parsing of the entries here!                                                             
        foreach ($feed_element->getElementsByTagName(Atom_Entry::ELEMENT_ENTRY) as $entry) {
            $extractedEntry = Atom_Entry::buildFromDOMElement($entry);
            
            if($extractedEntry === null) {
                // TODO! add log here, entry was skipped!
                continue;
            } 
            
            if($extractedEntry->getAuthor() === null) {
                $extractedEntry->addChildElement($author);
            }
            
            if($extractedEntry->getUpdated() === null) {
                $extractedEntry->addChildElement($updated);
            }
            
            $atom->addChildElement($extractedEntry);
        }                                                   
        return $atom;
    }
    
    /**
     * @return Atom_Feed
     */
    public function createCopyWithoutEntries() {
        $feed = new Atom_Feed();
        $feed->setNamespaces($this->getNamespaces());
        
        foreach ($this->childElements as $elm) {
            if($elm instanceof Atom_Entry) {
                continue;
            }
            $feed->addChildElement($elm);
        }
        return $feed;
    }
    
    public static function cmp_entries($a, $b) {
        $at = strtotime($a->getUpdated()->getUpdated());
        $bt = strtotime($b->getUpdated()->getUpdated());
        if ($at == $bt) {
            return 0;
        }
        return ($at < $bt) ? +1 : -1;
    }
}